﻿
Imports System.IO
Imports System.Text
Imports System.Threading
Imports filesystem
Imports applicationInfoOutput
Imports lock_t = lock(Of locks.monitorlock)

Namespace counter
    Public Module _counter
        Private Sub safe_increase(ByRef i As Int64, ByVal j As Int64, ByVal name As String)
            If Not Global.safe_increase(i, j) Then
                raiseError(errorHandle.errorType.warning, "overflow for count ", name, ", the value may not correct.")
            End If
        End Sub

        Private Class counterRecord
            Public name As String = Nothing

            Public count As Int64 = 0
            Public lastAverages() As Int64 = Nothing
            Public lastTimesTicks() As Int64 = Nothing
            Public calltimes As Int64 = 0
            Public sampleRate As Double = 0

            Public writeType As Int16 = 0
            Public registerTimeTicks As Int64 = 0
            Public intervalScale As Int64 = 0

            Public Function average() As Int64
                If calltimes > 0 Then
                    Return atomic.read(count) \ calltimes
                Else
                    Return npos
                End If
            End Function

            Private Function lastAverageCount() As Int64
                If debugassert(Not lastAverages Is Nothing AndAlso lastAverages.Length() > 0, _
                               "lastAverages is not initialized, the counter may not enabled lastAverage.") Then
                    Dim c As Int64 = 0
                    For i As Int64 = 0 To min(lastAverages.Length(), calltimes) - 1
                        safe_increase(c, atomic.read(lastAverages(i)), name)
                    Next
                    Return c
                Else
                    Return npos
                End If
            End Function

            Private Function lastAveragesLength() As Int64
                Return max(min(lastAverages.Length(), calltimes), 1)
            End Function

            Public Function lastAverage() As Int64
                Return lastAverageCount() \ lastAveragesLength()
            End Function

            Private Function timeInterval(ByVal start As Int64) As Double
                Dim t As Double
                t = (Now.ticks() - start) / milli_tick / second_milli / intervalScale
                Return ifoper(t <= 0, 1, t)
            End Function

            Public Function rate() As Int64
                Return Convert.ToInt64(count / timeInterval(registerTimeTicks))
            End Function

            Public Function lastRate() As Int64
                If debugassert(Not lastTimesTicks Is Nothing AndAlso lastTimesTicks.Length() > 0, _
                               "lastTimes is not initialized, the counter may not enabled lastRate.") Then
                    Dim c As Int64 = 0
                    Dim oldest As Int64 = 0
                    oldest = maxInt64
                    For i As Int64 = 0 To min(calltimes, lastTimesTicks.Length()) - 1
                        c = atomic.read(lastTimesTicks(i))
                        If c > 0 Then
                            If oldest > c Then
                                oldest = c
                            End If
                        End If
                    Next
                    If oldest < maxInt64 Then
                        Return Convert.ToInt64(lastAverageCount() / timeInterval(oldest))
                    Else
                        Return 0
                    End If
                Else
                    Return npos
                End If
            End Function

            Public Function hitSample() As Boolean
                If sampleRate = 1 Then
                    Return True
                Else
                    Return rnd(0, 1) < sampleRate
                End If
            End Function

            Public Const writeCount As Int16 = 1
            Public Const writeAverage As Int16 = 2
            Public Const writeLastAverage As Int16 = 4
            Public Const writeRate As Int16 = 8
            Public Const writeLastRate As Int16 = 16
        End Class

        'increase / decrease make a pair, so can get some detail information if set defaultflushtimes to an odd.
        'but ... it always be 0, 1, 0, 1, 0 ...
#If DEBUG Then
        Public Const writeFrequenceSeconds As Int32 = 60
#Else
        Public Const writeFrequenceSeconds As Int32 = 5 * 60
#End If
        Public Const defaultLastAverageLength As Int64 = 8196
        Public Const defaultLastRateLength As Int64 = 8196
        Public Const defaultIntervalScale As Int64 = 1
        Public Const defaultSampleRate As Double = 0.0001
        Public Const separator As String = character.comma + character.blank
        Public Const NOCOUNTERFILEMARK As String = "___NOCOUNTERFILE___"
        Public Const fileNameSuffix As String = ".counter" + normalSuffix
        Public Const criticalFilenameSuffix As String = ".counter" + criticalSuffix
        Public ReadOnly defaultCounterPath As String = combinepath(deployRoot, "counter")
        Public ReadOnly defaultCounterFilename As String = generateDefaultFilename(defaultCounterPath)
        Public ReadOnly writeFrequenceMinutes As Int32 = writeFrequenceSeconds \ 60
        Private lock As lock_t
        Private write_entry As singleentry

        Private Class internalCounter
            Public name As String = Nothing
            Public value As _do(Of Int64) = Nothing

            Public Sub New(ByVal name As String, ByVal value As _do(Of Int64),
                           Optional ByVal addTimeRange As Boolean = True)
                If addTimeRange Then
                    Me.name = strcat(name, "/", writeFrequenceMinutes, "MIN")
                Else
                    Me.name = name
                End If
                Me.value = value
            End Sub
        End Class
        Private ReadOnly internalCounters() As internalCounter

        Public Event writeCounter(ByVal name As String, ByVal count As Int64)
        Private counters As memblock(Of counterRecord) = Nothing
        Private filestream As autoflushFileStream = Nothing
        Private initialized As Boolean = False

        Private Class selfhealth
            Private Shared _increaseLatency As Int64 = 0
            Private Shared _increaseTimes As Int32 = 0
            Private Shared _writeLatency As Int64 = 0
            Private Shared _writeTimes As Int32 = 0

            Public Shared Sub recordIncreaseLatency(ByVal startticks As Int64)
                Interlocked.Add(_increaseLatency, Now.ticks() - startticks)
                Interlocked.Increment(_increaseTimes)
            End Sub

            Public Shared Sub recordWriteLatency(ByVal startticks As Int64)
                Interlocked.Add(_writeLatency, Now.ticks() - startticks)
                Interlocked.Increment(_writeTimes)
            End Sub

            Public Shared Function increaseLatency() As Int64
                Return return_clear(_increaseLatency)
            End Function

            Public Shared Function increaseTimes() As Int64
                Return return_clear(_increaseTimes)
            End Function

            Public Shared Function writeLatency() As Int64
                Return return_clear(_writeLatency)
            End Function

            Public Shared Function writeTimes() As Int64
                Return return_clear(_writeTimes)
            End Function
        End Class

        Public Function countersCount() As Int64
            Return counters.size()
        End Function

        Public Function counter(ByVal index As Int64, _
                                ByRef name As String, _
                                Optional ByRef count As Int64 = 0, _
                                Optional ByRef average As Int64 = 0, _
                                Optional ByRef lastAverage As Int64 = 0, _
                                Optional ByRef rate As Int64 = 0, _
                                Optional ByRef lastRate As Int64 = 0) As Boolean
            If Not counters.availableIndex(index) Then
                Return False
            Else
                With counters(index)
                    copy(name, .name)
                    If .writeType And counterRecord.writeCount Then
                        count = atomic.read(.count)
                    Else
                        count = npos
                    End If
                    If .writeType And counterRecord.writeAverage Then
                        average = .average()
                    Else
                        average = npos
                    End If
                    If .writeType And counterRecord.writeLastAverage Then
                        lastAverage = .lastAverage()
                    Else
                        lastAverage = npos
                    End If
                    If .writeType And counterRecord.writeRate Then
                        rate = .rate()
                    Else
                        rate = npos
                    End If
                    If .writeType And counterRecord.writeLastRate Then
                        lastRate = .lastRate()
                    Else
                        lastRate = npos
                    End If
                End With
                Return True
            End If
        End Function

        Private Function canSample(ByVal cr As counterRecord) As Boolean
            Return (Not cr.writeType And counterRecord.writeCount) AndAlso
                   (Not cr.writeType And counterRecord.writeRate) AndAlso
                   (Not cr.writeType And counterRecord.writeLastRate)
        End Function

        Public Function register(ByVal name As String,
                                 Optional ByVal writeCount As Boolean = True,
                                 Optional ByVal writeAverage As Boolean = False,
                                 Optional ByVal writeLastAverage As Boolean = False,
                                 Optional ByVal lastAverageLength As Int64 = defaultLastAverageLength,
                                 Optional ByVal writeRate As Boolean = False,
                                 Optional ByVal writeLastRate As Boolean = False,
                                 Optional ByVal lastRateLength As Int64 = defaultLastRateLength,
                                 Optional ByVal intervalScale As Int64 = defaultIntervalScale,
                                 Optional ByVal sampleRate As Double = defaultSampleRate,
                                 Optional ByVal autoInitialize As Boolean = True) As Int64
            If Not initialized AndAlso autoInitialize Then
                initial()
            End If
            strrplc(name, separator, "_")
            Dim rtn As Int64
            Dim cr As counterRecord = Nothing
            cr = New counterRecord()
            copy(cr.name, name)
            cr.intervalScale = ifoper(intervalScale <= 0, defaultIntervalScale, intervalScale)
            If writeCount Then
                cr.writeType = cr.writeType Or counterRecord.writeCount
            End If
            If writeAverage Then
                cr.writeType = cr.writeType Or counterRecord.writeAverage
            End If
            If writeLastAverage Then
                cr.writeType = cr.writeType Or counterRecord.writeLastAverage
            End If
            If writeLastAverage OrElse writeLastRate Then
                lastAverageLength = max(lastAverageLength, lastRateLength)
                ReDim cr.lastAverages(lastAverageLength - 1)
            End If
            If writeRate Then
                cr.writeType = cr.writeType Or counterRecord.writeRate
                cr.registerTimeTicks = Now.ticks()
            End If
            If writeLastRate Then
                cr.writeType = cr.writeType Or counterRecord.writeLastRate
                ReDim cr.lastTimesTicks(lastAverageLength - 1)
            End If
            If canSample(cr) Then
                cr.sampleRate = sampleRate
            Else
                cr.sampleRate = 1
            End If

            lock.wait()
            rtn = counters.size()
            counters += cr
            lock.release()
            raiseError("register counter " + name + " with intervalScale " + Convert.ToString(intervalScale) +
                       ", return ID " + Convert.ToString(rtn))

            Return rtn
        End Function

        Public Function registerCounter(ByVal name As String,
                                        Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name, autoInitialize:=autoInitialize)
        End Function

        Public Function registerAverage(ByVal name As String,
                                        Optional ByVal sampleRate As Double = defaultSampleRate,
                                        Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name, writeCount:=False, writeAverage:=True, sampleRate:=sampleRate,
                            autoInitialize:=autoInitialize)
        End Function

        Public Function registerLastAverage(ByVal name As String,
                                            Optional ByVal lastAverageLength As Int64 = defaultLastAverageLength,
                                            Optional ByVal sampleRate As Double = defaultSampleRate,
                                            Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name, writeCount:=False, writeLastAverage:=True,
                            lastAverageLength:=lastAverageLength, sampleRate:=sampleRate,
                            autoInitialize:=autoInitialize)
        End Function

        Public Function registerAverageAndLastAverage(ByVal name As String,
                                                      Optional ByVal lastAverageLength As Int64 = _
                                                          defaultLastAverageLength,
                                                      Optional ByVal sampleRate As Double = defaultSampleRate,
                                                      Optional ByVal autoInitialize As Boolean = True) _
                                                      As Int64
            Return register(name, writeCount:=False, writeAverage:=True, writeLastAverage:=True,
                            lastAverageLength:=lastAverageLength, sampleRate:=sampleRate,
                            autoInitialize:=autoInitialize)
        End Function

        Public Function registerRate(ByVal name As String,
                                     Optional ByVal intervalScale As Int64 = defaultIntervalScale,
                                     Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name, writeCount:=False, writeRate:=True,
                            intervalScale:=intervalScale, autoInitialize:=autoInitialize)
        End Function

        Public Function registerLastRate(ByVal name As String,
                                         Optional ByVal lastRateLength As Int64 = defaultLastRateLength,
                                         Optional ByVal intervalScale As Int64 = defaultIntervalScale,
                                         Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name, writeCount:=False, writeLastRate:=True, lastRateLength:=lastRateLength,
                            intervalScale:=intervalScale, autoInitialize:=autoInitialize)
        End Function

        Public Function registerRateAndLastRate(ByVal name As String,
                                                Optional ByVal lastRateLength As Int64 = defaultLastRateLength,
                                                Optional ByVal intervalScale As Int64 = defaultIntervalScale,
                                                Optional ByVal autoInitialize As Boolean = True) As Int64
            Return register(name,
                            writeCount:=False, writeRate:=True, writeLastRate:=True, lastRateLength:=lastRateLength,
                            intervalScale:=intervalScale, autoInitialize:=autoInitialize)
        End Function

        Private Function patchCounterMsg(ByVal name As String, ByVal count As Int64) As String
            RaiseEvent writeCounter(name, count)
            Return shortTime() + separator + name + separator + Convert.ToString(count)
        End Function

        Private Function patchCounterMsg(ByVal name As String, ByVal count As Int64, _
                                         ByVal writetype As String) As String
            If Not writetype Is Nothing Then
                Return patchCounterMsg(name + character.underline + writetype, count)
            Else
                Return patchCounterMsg(name, count)
            End If
        End Function

        Public Function breakupCounterMsg(ByVal msg As String, _
                                          ByRef time As String, ByRef name As String, ByRef count As Int64) As Boolean
            Dim index As Int64
            index = strindexof(msg, separator)
            If index = npos Then
                Return False
            Else
                time = strleft(msg, index)
                msg = strmid(msg, index + strlen(separator))

                index = strindexof(msg, separator)
                If index = npos Then
                    Return False
                Else
                    name = strleft(msg, index)
                    Dim str As String = Nothing
                    str = strmid(msg, index + strlen(separator))
                    If convertor.toint64(str, count) Then
                        Return True
                    Else
                        Return False
                    End If
                End If
            End If
        End Function

        Private Sub write(ByVal id As Int64)
            Dim startticks As Int64 = 0
            startticks = Now.ticks()
            Dim count As Int64 = 0
            Dim average As Int64 = 0
            Dim lastAverage As Int64 = 0
            Dim rate As Int64 = 0
            Dim lastRate As Int64 = 0
            Dim msg As String = Nothing
            Dim needwritetype As Boolean = False
            assert(counter(id, Nothing, count, average, lastAverage, rate, lastRate))
            With counters(id)
                needwritetype = bithelper._1count(.writeType) > 1

                If .writeType And counterRecord.writeCount Then
                    msg += patchCounterMsg(.name, count,
                                           ifoper(needwritetype, "COUNT", Nothing)) + newline.incode()
                End If
                If .writeType And counterRecord.writeAverage Then
                    msg += patchCounterMsg(.name, average,
                                           ifoper(needwritetype, "AVERAGE", Nothing)) + newline.incode()
                End If
                If .writeType And counterRecord.writeLastAverage Then
                    msg += patchCounterMsg(.name, lastAverage,
                                           ifoper(needwritetype, _
                                                  "LAST" + Convert.ToString(.lastAverages.Length()) + "AVERAGE", _
                                                  Nothing)) + newline.incode()
                End If
                If .writeType And counterRecord.writeRate Then
                    msg += patchCounterMsg(.name, rate,
                                           ifoper(needwritetype, "RATE", Nothing)) + newline.incode()
                End If
                If .writeType And counterRecord.writeLastRate Then
                    msg += patchCounterMsg(.name, lastRate,
                                           ifoper(needwritetype, _
                                                  "LAST" + Convert.ToString(.lastTimesTicks.Length()) + "RATE", _
                                                  Nothing)) + newline.incode()
                End If
            End With
            writefile(msg)
            selfhealth.recordWriteLatency(startticks)
        End Sub

        Private Sub writefile(ByVal s As String)
            Dim d() As Byte
            d = encoding.utf8.GetBytes(s)
            filestream.Write(d, 0, d.Length())
        End Sub

        Private Function return_clear(ByRef i As Int32) As Int64
            Dim j As Int64 = 0
            j = atomic.read(i)
            atomic.eva(i, 0)
            Return j
        End Function

        Private Function return_clear(ByRef i As Int64) As Int64
            Dim j As Int64 = 0
            j = atomic.read(i)
            atomic.eva(i, 0)
            Return j
        End Function

        Private Sub writeImpl()
            If write_entry.mark_in_use() Then
                lock.wait()
                For i As Int64 = 0 To counters.size() - 1
                    'remove some unused counters
                    If counters(i).calltimes > 0 Then
                        write(i)
                    End If
                Next
                lock.release()

                Dim msg As StringBuilder = Nothing
                msg = New StringBuilder()
                For i As Int64 = 0 To internalCounters.Length() - 1
                    msg.Append(patchCounterMsg(internalCounters(i).name, internalCounters(i).value()))
                    msg.Append(newline.incode())
                Next
                writefile(Convert.ToString(msg))
                write_entry.release()
            End If
        End Sub

        Private Sub write()
            Static lastWrite As Int64 = 0
            If lastWrite = 0 Then
                lastWrite = Now.seconds()
            End If
            If Now.seconds() - lastWrite >= writeFrequenceSeconds Then
                If Not filestream Is Nothing Then
                    If Not filestream.CanWrite() Then
                        initial(defaultCounterFilename, criticalFilenameSuffix)
                    End If
                    writeImpl()
                    filestream.Flush()
                End If
                lastWrite = Now.seconds()
            End If
        End Sub

        Public Function increase(ByVal id As Int64, ByVal dc As lazier(Of Int64)) As Boolean
            If counters.availableIndex(id) Then
                Dim cr As counterRecord = Nothing
                cr = counters(id)
                If cr.hitSample() Then
                    Dim startticks As Int64 = 0
                    startticks = Now.ticks()
                    Dim c As Int64 = 0
                    c = +dc
                    With cr
                        Dim index As Int64 = 0
                        index = Interlocked.Increment(.calltimes) - 1
                        If (.writeType And counterRecord.writeCount) OrElse
                           (.writeType And counterRecord.writeAverage) OrElse
                           (.writeType And counterRecord.writeRate) Then
                            Interlocked.Add(.count, c)
                        End If
                        If (.writeType And counterRecord.writeLastAverage) OrElse
                           (.writeType And counterRecord.writeLastRate) Then
                            .lastAverages(index Mod .lastAverages.Length()) = c
                        End If
                        If (.writeType And counterRecord.writeLastRate) Then
                            .lastTimesTicks(index Mod .lastTimesTicks.Length()) = startticks
                        End If
                    End With
                    selfhealth.recordIncreaseLatency(startticks)
                End If

                Return True
            Else
                Return False
            End If
        End Function

        Public Function increase(ByVal id As Int64, ByVal dc As _do(Of Int64)) As Boolean
            Return increase(id, New lazier(Of Int64)(dc))
        End Function

        Public Function increase(ByVal id As Int64, Optional ByVal c As Int64 = 1) As Boolean
            Return increase(id, do_(c))
        End Function

        Public Function decrease(ByVal id As Int64, ByVal dc As lazier(Of Int64)) As Boolean
            Return increase(id,
                            New lazier(Of Int64)(Function() As Int64
                                                     Return -(+dc)
                                                 End Function))
        End Function

        Public Function decrease(ByVal id As Int64, ByVal dc As _do(Of Int64)) As Boolean
            Return decrease(id, New lazier(Of Int64)(dc))
        End Function

        Public Function decrease(ByVal id As Int64, Optional ByVal c As Int64 = 1) As Boolean
            Return decrease(id, do_(c))
        End Function

        Private Class ts
            <ThreadStatic()> Public Shared startticks As Int64
        End Class

        Public Sub recordTimeBegin()
            ts.startticks = Now.ticks()
        End Sub

        Public Function recordTimeStartticks() As Int64
            assert(ts.startticks > 0)
            Return ts.startticks
        End Function

        Public Function recordTimeMs(ByVal id As Int64, ByVal startMs As Int64) As Boolean
            Return increase(id, Now.milliseconds() - startMs)
        End Function

        Public Function recordTimeMs(ByVal id As Int64) As Boolean
            Return recordTimeMs(id, ticksToMilliseconds(recordTimeStartticks()))
        End Function

        Public Function recordTimeTicks(ByVal id As Int64, ByVal startticks As Int64) As Boolean
            Return increase(id, Now.ticks() - startticks)
        End Function

        Public Function recordTimeTicks(ByVal id As Int64) As Boolean
            Return recordTimeTicks(id, recordTimeStartticks())
        End Function

        Public Sub initial(ByVal filename As String, Optional ByVal suffix As String = fileNameSuffix)
            generateFileStream(filestream, filename, suffix, NOCOUNTERFILEMARK)
            initialized = True
        End Sub

        Public Sub initial()
            initial(defaultCounterFilename)
        End Sub

        Private Sub unhandledException(ByVal ex As Exception)
            finalize()
        End Sub

        Public Sub finalize()
            If Not filestream Is Nothing Then
                writeImpl()
                closestream(filestream)
            End If
        End Sub

#If PRESERVE Then
        Private Function spinlock_wait_times() As Int64
            Return return_clear(locks.spinlock.waittimes)
        End Function

        Private Function spinlock_wait_rounds() As Int64
            Return return_clear(locks.spinlock.waitrounds)
        End Function

        Private Function spinlock_wait_ticks() As Int64
            Return return_clear(locks.spinlock.waitticks)
        End Function

        Private Function spinwait_yield_times() As Int64
            Return return_clear(spinwait.success_yield_times)
        End Function
#End If

        Sub New()
            If Not isdebugmode() Then
                registerUnhandledException(AddressOf unhandledException)
            End If
            counters = New memblock(Of counterRecord)()
#If PRESERVE Then
                New internalCounter("SPINLOCK_WAIT_TIMES", AddressOf spinlock_wait_times),
                New internalCounter("SPINLOCK_WAIT_ROUNDS", AddressOf spinlock_wait_rounds),
                New internalCounter("SPINLOCK_WAIT_TICKS", AddressOf spinlock_wait_ticks),
                New internalCounter("SPINWAIT_SUCCESS_YIELD_TIMES", AddressOf spinwait_yield_times),
#End If
            internalCounters = New internalCounter() {
                New internalCounter("COUNTER_INCREASE_LATENCY_TICKS", AddressOf selfhealth.increaseLatency),
                New internalCounter("COUNTER_INCREASE_TIMES", AddressOf selfhealth.increaseTimes),
                New internalCounter("COUNTER_WRITE_LATENCY_TICKS", AddressOf selfhealth.writeLatency),
                New internalCounter("COUNTER_WRITE_TIMES", AddressOf selfhealth.writeTimes),
                New internalCounter("PRIVATE_BYTES", AddressOf performance.privateBytesUsage, False),
                New internalCounter("VIRTUAL_BYTES", AddressOf performance.virtualBytesUsage, False),
                New internalCounter("WORKSET_BYTES", AddressOf performance.worksetBytesUsage, False),
                New internalCounter("PROCESSOR_USAGE",
                                    Function() As Int64
                                        Return Convert.ToInt64(performance.processorUsage() * 100)
                                    End Function,
                                    False),
                New internalCounter("RECENT_PROCESSOR_USAGE",
                                    Function() As Int64
                                        Return Convert.ToInt64(performance.recentProcessorUsage() * 100)
                                    End Function)}
            queueInBackendThread(AddressOf write)
        End Sub
    End Module
End Namespace
